/*
 *    Copyright (C) 2011-2012 Hai Bison
 *
 *    See the file LICENSE at the root directory of this project for copying
 *    permission.
 */

package group.pals.mailgate.threads;

import com.sun.mail.imap.IMAPMessage;
import group.pals.mailgate.beans.ItemEmailAccount;
import group.pals.mailgate.beans.ItemEmailMessage;
import group.pals.mailgate.beans.ItemEmailProvider;
import group.pals.mailgate.preferences.Preferences;
import group.pals.threads.BaseThread;
import group.pals.threads.listeners.ThreadListener;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.activation.DataHandler;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Flags;
import javax.mail.Flags.Flag;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.search.FlagTerm;

/**
 *
 * @author Hai Bison
 */
public class EmailReader extends BaseThread {

    public static final boolean Debug = false;

    /*----------------------------------------------------------------------------
     * KEYS
     */
    public static final int KeyNewEmailMessages = 0;
    public static final int KeyMessage = 1;
    public static final int KeyManualReport = 2;
    private final ItemEmailAccount EmailAccount;
    private final ItemEmailProvider EmailProvider;
    private final List<ItemEmailMessage> EmailMessages;
    private int connectionTimeout;
    private int emailCheckTimePeriod;
    private boolean forceCheck;

    public EmailReader(String name, ItemEmailAccount emailAccount, ItemEmailProvider emailProvider) {
        super(name);

        this.EmailAccount = emailAccount;
        this.EmailProvider = emailProvider;
        this.EmailMessages = new ArrayList<ItemEmailMessage>();

        this.connectionTimeout = Preferences.DefConnectionTimeout;
        this.emailCheckTimePeriod = Preferences.DefEmailCheckTimePeriod;
        this.forceCheck = false;
    }

    /**
     * @return the e-mail address.
     */
    @Override
    public String toString() {
        return EmailAccount.getEmailAddress();
    }

    @Override
    public void run() {
        try {
            while (!isInterrupted() || wasForceCheck()) {
                try {
                    //read mails
                    notifyMessage("Getting new e-mails...");
                    List<ItemEmailMessage> unreadEmailMessages = getUnreadEmailMessages();

                    waitWhileSuspending();
                    if (isInterrupted()) {
                        break;
                    }

                    /* compare to current mails to get new ones,
                     * if find them, notify user
                     */
                    notifyMessage("Checking...");
                    List<ItemEmailMessage> newEmailMessages = updateEmailMessages(unreadEmailMessages);

                    waitWhileSuspending();
                    if (isInterrupted()) {
                        break;
                    }

                    //notify user
                    if (newEmailMessages.isEmpty()) {
                        notifyMessage("No new e-mails");
                        notifyManualReport();
                    } else {
                        notifyMessage(String.format(
                                "New %,d e-mail%s", newEmailMessages.size(), newEmailMessages.size() > 1 ? "s" : ""));
                        for (ThreadListener listener : Listeners) {
                            listener.threadSignaled(this, KeyNewEmailMessages, newEmailMessages);
                        }
                    }

                    System.gc();
                } catch (Exception ex) {
                    notifyException(ex);
                }

                //sleep
                try {
                    sleep(emailCheckTimePeriod);
                } catch (InterruptedException ie) {
                    if (wasForceCheck()) {
                        continue;
                    } else {
                        interrupt();
                    }
                }
            }//while not interrupted
        } finally {
            //clear interrupted flag
            interrupted();
            //notify terminate
            notifyTerminated();
        }
    }

    /**
     * Connects and get all unread messages.
     * @return list of unread messages.
     * @throws MessagingException
     */
    private List<ItemEmailMessage> getUnreadEmailMessages() throws MessagingException {
        List<ItemEmailMessage> result = new ArrayList<ItemEmailMessage>();

        Properties config = new Properties();
        config.setProperty("mail.store.protocol", "imaps");

        //To use TLS (for POP3)
//    props.put("mail.imap.auth", "true"); 
//    props.put("mail.imap.starttls.enable", "true");

        // set this session up to use SSL for IMAP connections
        config.setProperty("mail.imaps.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        // don't fallback to normal IMAP connections on failure.
        config.setProperty("mail.imaps.socketFactory.fallback", "false");
        // use the simap port for imap/ssl connections.
        config.setProperty("mail.imaps.socketFactory.host", EmailProvider.getImapHost());
        config.setProperty("mail.imaps.socketFactory.port", Integer.toString(EmailProvider.getImapPort()));
        // use the timeout
        config.setProperty("mail.imaps.timeout", Integer.toString(connectionTimeout));
        config.setProperty("mail.imaps.connectionpooltimeout", Integer.toString(connectionTimeout));
        config.setProperty("mail.imaps.connectiontimeout", Integer.toString(connectionTimeout));

        Session session = Session.getInstance(config);
        Store store = session.getStore();
        store.connect(EmailProvider.getImapHost(), EmailProvider.getImapPort(),
                EmailAccount.getEmailAddress(), EmailAccount.getPassword());
        try {
            Folder inbox = store.getFolder("inbox");
            inbox.open(Folder.READ_WRITE);
            FlagTerm ft = new FlagTerm(new Flags(Flags.Flag.SEEN), false);
            Message[] messages = inbox.search(ft);
            //if user set flags to messages, then reload the messages
            if (checkFlags(messages)) {
                if (Debug) {
                    System.out.printf("%s: Check flag for messages = true\n", getClass().getSimpleName());
                }
                messages = inbox.search(ft);
            }
            for (Message message : messages) {
                waitWhileSuspending();
                if (isInterrupted()) {
                    break;
                }

                /*
                 * to avoid message marking as read after loading its content
                 */
                if (message instanceof IMAPMessage) {
                    ((IMAPMessage) message).setPeek(true);
                }

                /*
                 * Message-ID
                 * this can throw NullPointerException,
                 * if so, just notify user that e-mail provider of sender does not
                 * provide Message-ID.
                 */
                String messageID;
                try {
                    messageID = message.getHeader("Message-ID")[0];
                } catch (NullPointerException e) {
                    throw new MessagingException("E-mail message does not have Message ID");
                }

                /*
                 * Senders
                 */
                String senders = "";
                Address[] addresses = message.getFrom();
                if (addresses != null) {
                    for (Address address : addresses) {
                        senders += address.toString() + "\n";
                    }
                    senders = senders.trim().replaceAll("\n", "; ").trim();
                }

                /*
                 * Subject
                 */
                String subject = message.getSubject();
                if (subject == null) {
                    subject = "";
                } else {
                    subject = subject.trim();
                }

                /*
                 * attachment & textSnippet
                 */
                boolean hasAttachment = false;
                String textSnippet = null;
                try {
                    Object content = message.getContent();
                    if (content instanceof Multipart) {
                        Multipart multipart = (Multipart) content;
                        for (int i = 0; i < multipart.getCount(); i++) {
                            if (Debug) {
                                System.out.printf("%s: %s -> %s\n",
                                        getClass().getSimpleName(), subject,
                                        multipart.getBodyPart(i).getDisposition());
                            }

                            BodyPart bodyPart = multipart.getBodyPart(i);

                            if (BodyPart.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
                                hasAttachment = true;
                            } else {
                                InputStream inputStream = bodyPart.getInputStream();
                                try {
                                    byte[] buf = new byte[128];
                                    int read = inputStream.read(buf);
                                    if (read > 0) {
                                        textSnippet = new String(buf, 0, read);
                                    }
                                } finally {
                                    inputStream.close();
                                }
                            }
                        }//for
                    }// MultiPart
                    else if (content instanceof String) {
                        textSnippet = (String) content;
                    }// String
                    else if (content instanceof InputStream) {
                        byte[] buf = new byte[128];
                        int read = ((InputStream) content).read(buf);
                        if (read > 0) {
                            textSnippet = new String(buf, 0, read);
                        }
                    }// InputStream
                } catch (Exception ex) {
                } finally {
                    /*
                     * 20111105:
                     * - fix: some e-mails are marked as read after this check of attachment
                     * - we don't need to check flag, because it will make a connection to server,
                     * the JavaMail does not save message's flags in local machine.
                     */
                    if (!(message instanceof IMAPMessage)) {
                        message.setFlag(Flag.SEEN, false);
                    }
                }

                ItemEmailMessage emailMessage = new ItemEmailMessage(messageID,
                        senders, subject, message.getReceivedDate(), textSnippet, hasAttachment);
                result.add(emailMessage);
            }
        } finally {
            store.close();
        }

        return result;
    }

    /**
     * Checks and sets flags for message.
     * @param messages
     * @throws MessagingException 
     * @return {@code true} if one of {@code messages} need to be reload, because
     * user set its flags.
     */
    private boolean checkFlags(Message[] messages) throws MessagingException {
        /**
         * If no messages have one of these flags, then we don't need to reload
         * messages again.
         */
        final List<Flags.Flag> FlagsNeedUpdate = new ArrayList<Flag>();
        FlagsNeedUpdate.add(Flag.SEEN);
        FlagsNeedUpdate.add(Flag.DELETED);

        boolean result = false;

        for (Message message : messages) {
            waitWhileSuspending();
            if (isInterrupted()) {
                break;
            }

            /*
             * Message-ID
             * this can throw NullPointerException,
             * if so, just notify user that e-mail provider of sender does not
             * provide Message-ID
             */
            String messageID;
            try {
                messageID = message.getHeader("Message-ID")[0];
            } catch (NullPointerException e) {
                throw new MessagingException("E-mail message does not have Message ID");
            }

            for (ItemEmailMessage currentEmailMessage : EmailMessages) {
                waitWhileSuspending();
                if (isInterrupted()) {
                    break;
                }

                if (currentEmailMessage.getID().equals(messageID) && currentEmailMessage.getFlags().size() > 0) {
                    Map<Flag, Boolean> mapFlags = currentEmailMessage.getFlags();
                    for (Flags.Flag flag : mapFlags.keySet()) {
                        //check if "flag" need to be reloaded
                        if ((FlagsNeedUpdate.indexOf(flag) >= 0) && mapFlags.get(flag)) {
                            if (Debug) {
                                System.out.printf("%s: Message \"%s\" -> flag \"%s\" = %s\n",
                                        getClass().getSimpleName(), message.getSubject(), flag, mapFlags.get(flag));
                            }
                            message.setFlag(flag, mapFlags.get(flag));
                            result = true;
                        }
                    }
                    break;
                }
            }//for ItemEmailMessage
        }//for Message

        return result;
    }

    /**
     * Updates new e-mail messages, and returns the new ones if there are them.
     * @param emailMessages list of {@link ItemEmailMessage}
     * @return list of new messages, can be empty.
     */
    private List<ItemEmailMessage> updateEmailMessages(List<ItemEmailMessage> emailMessages) {
        List<ItemEmailMessage> result = new ArrayList<ItemEmailMessage>();

        for (ItemEmailMessage emailMessage : emailMessages) {
            waitWhileSuspending();
            if (isInterrupted()) {
                break;
            }

            boolean found = false;
            for (ItemEmailMessage currentEmailMessage : this.EmailMessages) {
                waitWhileSuspending();
                if (isInterrupted()) {
                    break;
                }

                if (currentEmailMessage.getID().equals(emailMessage.getID())) {
                    found = true;
                    break;
                }
            }//for currentEmailMessage
            if (found) {
                continue;
            }

            /*
             * Set new here, but next time we don't need to unset this attribute.
             * Because we clear all current messages, and add new ones - which their
             * default "newMessage" are false.
             */
            emailMessage.setNewMessage(true);
            result.add(emailMessage);
        }//for emailMessage

        this.EmailMessages.clear();
        this.EmailMessages.addAll(emailMessages);

        return result;
    }

    /**
     * @return the connectionTimeout
     */
    public int getConnectionTimeout() {
        return connectionTimeout;
    }

    /**
     * @param connectionTimeout the connectionTimeout to set
     */
    public void setConnectionTimeout(int connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    /**
     * @return the emailCheckTimePeriod
     */
    public int getEmalCheckTimePeriod() {
        return emailCheckTimePeriod;
    }

    /**
     * @param emailCheckTimePeriod the emailCheckTimePeriod to set
     */
    public void setEmailCheckTimePeriod(int emailCheckTimePeriod) {
        this.emailCheckTimePeriod = emailCheckTimePeriod;
    }

    /**
     * @return the emailAccount
     */
    public ItemEmailAccount getEmailAccount() {
        return EmailAccount;
    }

    /**
     * @return the emailProvider
     */
    public ItemEmailProvider getEmailProvider() {
        return EmailProvider;
    }

    /**
     * @return the clone of emailMessages
     */
    public List<ItemEmailMessage> cloneEmailMessages() {
        synchronized (EmailMessages) {
            List<ItemEmailMessage> tmp = new ArrayList<ItemEmailMessage>();
            tmp.addAll(EmailMessages);
            return tmp;
        }
    }

    /**
     * Gets <code>forceCheck</code> status, and reset it to <code>false</code>.
     * @return the status of <code>forceCheck</code>
     */
    private boolean wasForceCheck() {
        try {
            return forceCheck;
        } finally {
            forceCheck = false;
        }
    }

    /**
     * Sets <code>forceCheck</code> to <code>true</code>, then interrupts the thread.
     */
    public void doForceCheck() {
        if (getState() == State.TIMED_WAITING) {
            forceCheck = true;
            interrupt();
        }
    }

    /*----------------------------------------------------------------------------
     * Event Notifiers
     */
    private void notifyMessage(Object message) {
        waitWhileSuspending();
        if (isInterrupted()) {
            return;
        }

        for (ThreadListener listener : Listeners) {
            listener.threadSignaled(this, KeyMessage, message);
        }
    }

    private void notifyManualReport() {
        waitWhileSuspending();
        if (isInterrupted()) {
            return;
        }

        for (ThreadListener listener : Listeners) {
            listener.threadSignaled(this, KeyManualReport, null);
        }
    }
}
